<!doctype html>
<html lang="zh"><head>
<title>浏览器-高性能渲染十万条数据(虚拟列表) - 糖羽仙</title>
<meta charset="UTF-8">
<meta name="keywords" content="blog ACG JavaScript VUE IT">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5">

<link rel="shortcut icon" href="/images/head/head.jpg" type="image/jpeg" />
<meta name="description" content="对于一次性插入大量数据的情况，一般有两种做法：时间分片,虚拟列表">
<meta property="og:type" content="article">
<meta property="og:title" content="浏览器-高性能渲染十万条数据(虚拟列表)">
<meta property="og:url" content="https://www.tangyuxian.com/2021/04/16/%E5%89%8D%E7%AB%AF/%E6%B5%8F%E8%A7%88%E5%99%A8/%E6%B5%8F%E8%A7%88%E5%99%A8-%E9%AB%98%E6%80%A7%E8%83%BD%E6%B8%B2%E6%9F%93%E5%8D%81%E4%B8%87%E6%9D%A1%E6%95%B0%E6%8D%AE-%E8%99%9A%E6%8B%9F%E5%88%97%E8%A1%A8/index.html">
<meta property="og:site_name" content="糖羽仙">
<meta property="og:description" content="对于一次性插入大量数据的情况，一般有两种做法：时间分片,虚拟列表">
<meta property="og:locale" content="zh_CN">
<meta property="og:image" content="https://cdn.tangyuxian.com/PicGoImages/20210420102654.gif">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2019/10/29/16e15195cf16a558?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2019/10/29/16e15197c273cbd9?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2019/10/29/16e1519a393dee2c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2019/10/29/16e151e017d7bba3?imageslim">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2019/10/29/16e1519f1e121be9?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2019/10/29/16e151e96584b690?imageslim">
<meta property="og:image" content="https://user-gold-cdn.xitu.io/2019/10/29/16e151a59317cae7?imageView2/0/w/1280/h/960/format/webp/ignore-error/1">
<meta property="og:image" content="https://cdn.tangyuxian.com/PicGoImages/20210416162829.gif">
<meta property="article:published_time" content="2021-04-16T08:26:16.000Z">
<meta property="article:modified_time" content="2022-11-29T05:34:22.780Z">
<meta property="article:author" content="tangyuxian">
<meta property="article:tag" content="转载">
<meta property="article:tag" content="浏览器">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://cdn.tangyuxian.com/PicGoImages/20210420102654.gif">

<link rel="stylesheet" href="/lib/fancybox/fancybox.css">
<link rel="stylesheet" href="/lib/mdui_043tiny/mdui.css">


<link rel="stylesheet" href="/lib/iconfont/iconfont.css?v=1763992124143">

    <link rel="stylesheet" href="//at.alicdn.com/t/font_2421060_8z08qcz5sq3.css">

<link rel="stylesheet" href="/css/style.css?v=1763992124143">




    
        <link rel="stylesheet" href="/custom.css?v=1763992124143">
    
        <link rel="stylesheet" href="/themes/umaru/index.css?v=1763992124143">
    
        <link rel="stylesheet" href="/themes/asuka/index.css?v=1763992124143">
    



<script src="/lib/mdui_043tiny/mdui.js" async></script>
<script src="/lib/fancybox/fancybox.umd.js" async></script>
<script src="/lib/lax.min.js" async></script>


<script async src="/js/app.js?v=1763992124143"></script>

 

<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-1Z7TFL96ZH"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'G-1Z7TFL96ZH');
</script>


<link rel="stylesheet"  href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/atom-one-dark.min.css">
<!-- hexo injector head_end start -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hexo-shiki-plugin@latest/lib/codeblock.css">
<style>:root{--hl-color:#e1e4e8;--hl-bg:#24292e;--hltools-bg:#1f2428;--hltools-color:#c5c5c5;--hlnumber-bg:#24292e;--hlnumber-color:#444d56;--hlscrollbar-bg:#32383e;--hlexpand-bg:linear-gradient(180deg,rgba(36,41,46,0.1),rgba(36,41,46,0.9))}</style><!-- hexo injector head_end end --><meta name="generator" content="Hexo 7.3.0"></head><body class="nexmoe mdui-drawer-body-left"><div id="nexmoe-background"><div class="nexmoe-bg" style="background-image: url(/images/background/xiaomai.jpg)"></div><div class="nexmoe-character"></div><div class="mdui-appbar mdui-shadow-0"><div class="mdui-toolbar"><a class="mdui-btn mdui-btn-icon mdui-ripple" mdui-drawer="{target: &#039;#drawer&#039;, swipe: true}" title="menu"><i class="mdui-icon nexmoefont icon-menu"></i></a><div class="mdui-toolbar-spacer"></div><a class="mdui-btn mdui-btn-icon" href="/" title="tangyuxian"><img src="/images/head/head.jpg" alt="tangyuxian"></a></div></div></div><div id="nexmoe-header"><div class="nexmoe-drawer mdui-drawer" id="drawer">
    <div class="nexmoe-avatar mdui-ripple">
        <a href="/" title="tangyuxian">
            <img src="/images/head/head.jpg" alt="tangyuxian" alt="tangyuxian">
        </a>
    </div>
    <div class="nexmoe-count">
        <div class="nexmoe-count-item"><span>文章</span>84 <div class="item-radius"></div><div class="item-radius item-right"></div> </div>
        <div class="nexmoe-count-item"><span>标签</span>38<div class="item-radius"></div><div class="item-radius item-right"></div></div>
        <div class="nexmoe-count-item"><span>分类</span>5<div class="item-radius"></div><div class="item-radius item-right"></div></div>
    </div>
    <div class="nexmoe-list mdui-list" mdui-collapse="{accordion: true}">
        
        <a class="nexmoe-list-item mdui-list-item mdui-ripple false" href="/" title="回到首页">
            <i class="mdui-list-item-icon nexmoefont icon-meishi"></i>
            <div class="mdui-list-item-content">
                回到首页
            </div>
        </a>
        
        <a class="nexmoe-list-item mdui-list-item mdui-ripple false" href="/archives.html" title="文章归档">
            <i class="mdui-list-item-icon nexmoefont icon-hanbao1"></i>
            <div class="mdui-list-item-content">
                文章归档
            </div>
        </a>
        
        <a class="nexmoe-list-item mdui-list-item mdui-ripple false" href="/about.html" title="关于糖糖">
            <i class="mdui-list-item-icon nexmoefont icon-jiubei1"></i>
            <div class="mdui-list-item-content">
                关于糖糖
            </div>
        </a>
        
        <a class="nexmoe-list-item mdui-list-item mdui-ripple false" href="/friend.html" title="我的朋友">
            <i class="mdui-list-item-icon nexmoefont icon-cola"></i>
            <div class="mdui-list-item-content">
                我的朋友
            </div>
        </a>
        
    </div>
    
    
        
        <div class="nexmoe-widget-wrap">
    <div class="nexmoe-widget nexmoe-search">
         
            <form id="search_form" action_e="https://cn.bing.com/search?q=site:tangyuxian.com" onsubmit="return search();">
                <label><input id="search_value" name="q" type="search" placeholder="搜索"></label>
            </form>
         
    </div>
</div>




    
        
        <div class="nexmoe-widget-wrap">
	<div class="nexmoe-widget nexmoe-social">
		<a
			class="mdui-ripple"
			href="http://wpa.qq.com/msgrd?v=3&uin=2120252100&site=qq&menu=yes"
			target="_blank"
			mdui-tooltip="{content: 'QQ'}"
			style="
				color: rgb(64, 196, 255);
				background-color: rgba(64, 196, 255, .1);
			"
		>
			<i
				class="nexmoefont icon-QQ"
			></i> </a
		><a
			class="mdui-ripple"
			href="mailto:tangyuxian@vip.qq.com"
			target="_blank"
			mdui-tooltip="{content: 'mail'}"
			style="
				color: rgb(249,8,8);
				background-color: rgba(249,8,8,.1);
			"
		>
			<i
				class="nexmoefont icon-mail-fill"
			></i> </a
		><a
			class="mdui-ripple"
			href="https://blog.csdn.net/qq_40621378?type=blog"
			target="_blank"
			mdui-tooltip="{content: 'CSDN'}"
			style="
				color: rgb(199,29,35);
				background-color: rgba(199,29,35,.1);
			"
		>
			<i
				class="nexmoefont icon-csdn"
			></i> </a
		><a
			class="mdui-ripple"
			href="https://www.cnblogs.com/lovetangyuxian/"
			target="_blank"
			mdui-tooltip="{content: '博客园'}"
			style="
				color: rgb(66, 214, 29);
				background-color: rgba(66, 214, 29, .1);
			"
		>
			<i
				class="nexmoefont icon-bokeyuan"
			></i> </a
		><a
			class="mdui-ripple"
			href="https://github.com/tangyuxian/"
			target="_blank"
			mdui-tooltip="{content: 'GitHub'}"
			style="
				color: rgb(25, 23, 23);
				background-color: rgba(25, 23, 23, .15);
			"
		>
			<i
				class="nexmoefont icon-github"
			></i> </a
		><a
			class="mdui-ripple"
			href="https://gitee.com/tangyuxian"
			target="_blank"
			mdui-tooltip="{content: 'gitee'}"
			style="
				color: rgb(255, 255, 255);
				background-color: rgb(199,29,35);
			"
		>
			<i
				class="nexmoefont icon-mayun"
			></i> </a
		>
	</div>
</div>

    
        
        
  <div class="nexmoe-widget-wrap">
    <h3 class="nexmoe-widget-title">文章分类</h3>
    <div class="nexmoe-widget">

      <ul class="category-list">

        


        

        

        <li class="category-list-item">
          <a class="category-list-link" href="/categories/其它/">其它</a>
          <span class="category-list-count">2</span>
        </li>

        

        <li class="category-list-item">
          <a class="category-list-link" href="/categories/前端/">前端</a>
          <span class="category-list-count">62</span>
        </li>

        

        <li class="category-list-item">
          <a class="category-list-link" href="/categories/后端/">后端</a>
          <span class="category-list-count">8</span>
        </li>

        

        <li class="category-list-item">
          <a class="category-list-link" href="/categories/开发工具/">开发工具</a>
          <span class="category-list-count">11</span>
        </li>

        

        <li class="category-list-item">
          <a class="category-list-link" href="/categories/服务器端/">服务器端</a>
          <span class="category-list-count">1</span>
        </li>

        
      </ul>

    </div>
  </div>


    
        
        
  <div class="nexmoe-widget-wrap">
    <div id="randomtagcloud" class="nexmoe-widget tagcloud nexmoe-rainbow">
      <a href="/tags/JavaScript/" style="font-size: 15.56px;">JavaScript</a> <a href="/tags/Jetbrains/" style="font-size: 10px;">Jetbrains</a> <a href="/tags/TypeScript/" style="font-size: 10px;">TypeScript</a> <a href="/tags/UI%E5%BA%93/" style="font-size: 13.33px;">UI库</a> <a href="/tags/centos/" style="font-size: 10px;">centos</a> <a href="/tags/css/" style="font-size: 11.11px;">css</a> <a href="/tags/docker/" style="font-size: 10px;">docker</a> <a href="/tags/echarts/" style="font-size: 10px;">echarts</a> <a href="/tags/elementPlus/" style="font-size: 13.33px;">elementPlus</a> <a href="/tags/es6/" style="font-size: 14.44px;">es6</a> <a href="/tags/git/" style="font-size: 13.33px;">git</a> <a href="/tags/github/" style="font-size: 10px;">github</a> <a href="/tags/html/" style="font-size: 10px;">html</a> <a href="/tags/http/" style="font-size: 13.33px;">http</a> <a href="/tags/java/" style="font-size: 11.11px;">java</a> <a href="/tags/markerdown/" style="font-size: 11.11px;">markerdown</a> <a href="/tags/nginx/" style="font-size: 10px;">nginx</a> <a href="/tags/node/" style="font-size: 13.33px;">node</a> <a href="/tags/vite/" style="font-size: 10px;">vite</a> <a href="/tags/vue/" style="font-size: 18.89px;">vue</a> <a href="/tags/webpack/" style="font-size: 10px;">webpack</a> <a href="/tags/xuex/" style="font-size: 10px;">xuex</a> <a href="/tags/%E4%BA%8B%E4%BB%B6%E6%B5%81/" style="font-size: 10px;">事件流</a> <a href="/tags/%E4%BA%92%E8%81%94%E7%BD%91/" style="font-size: 10px;">互联网</a> <a href="/tags/%E5%8A%A0%E5%AF%86/" style="font-size: 12.22px;">加密</a> <a href="/tags/%E5%8E%9F%E5%88%9B/" style="font-size: 20px;">原创</a> <a href="/tags/%E5%B7%A5%E5%85%B7/" style="font-size: 13.33px;">工具</a> <a href="/tags/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/" style="font-size: 12.22px;">微信小程序</a> <a href="/tags/%E6%80%BB%E7%BB%93/" style="font-size: 13.33px;">总结</a> <a href="/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/" style="font-size: 10px;">数据结构</a> <a href="/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8/" style="font-size: 10px;">服务器</a> <a href="/tags/%E6%B5%8F%E8%A7%88%E5%99%A8/" style="font-size: 17.78px;">浏览器</a> <a href="/tags/%E7%AE%97%E6%B3%95/" style="font-size: 14.44px;">算法</a> <a href="/tags/%E7%BC%96%E8%A7%A3%E7%A0%81/" style="font-size: 10px;">编解码</a> <a href="/tags/%E7%BE%8E%E5%8C%96/" style="font-size: 12.22px;">美化</a> <a href="/tags/%E8%A7%86%E9%A2%91/" style="font-size: 11.11px;">视频</a> <a href="/tags/%E8%AE%B0%E5%BD%95/" style="font-size: 11.11px;">记录</a> <a href="/tags/%E8%BD%AC%E8%BD%BD/" style="font-size: 16.67px;">转载</a>
    </div>
    
      <script>
        var maxTagcloud = parseInt(17);
        var tags_length = parseInt(38);
        var tags_arr = [];
        for(var i = 0; i < tags_length; i++){
          tags_arr.push(i);
        }
        tags_arr.sort(function (l, r) {
          return Math.random() > 0.5 ? -1 : 1;
        });
        tags_arr = tags_arr.slice(0, maxTagcloud < tags_length ? tags_length - maxTagcloud : 0);
        for(var tag_i = 0; tag_i < tags_arr.length; tag_i++){
          document.getElementById("randomtagcloud").children[tags_arr[tag_i]].style.display = 'none';
        }
      </script>
    
  </div>

    
        
        
        
  <div class="nexmoe-widget-wrap">
    <h3 class="nexmoe-widget-title">文章归档</h3>
    <div class="nexmoe-widget">
      <ul class="archive-list"><li class="archive-list-item"><a class="archive-list-link" href="/archives/2025/">2025</a><span class="archive-list-count">5</span></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2023/">2023</a><span class="archive-list-count">2</span></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2022/">2022</a><span class="archive-list-count">23</span></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2021/">2021</a><span class="archive-list-count">54</span></li></ul>
    </div>
  </div>



    
        
        
  <div class="nexmoe-widget-wrap">
    <h3 class="nexmoe-widget-title">最新文章</h3>
    <div class="nexmoe-widget">
      <ul>
        
          <li>
            <a href="/2025/11/12/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7/%E7%BE%8E%E5%8C%96/%E7%BE%8E%E5%8C%96-%E7%BD%91%E9%A1%B5%E4%B8%BB%E9%A2%98%E5%88%87%E6%8D%A2%E7%9A%84%E6%B8%90%E5%8F%98%E5%8A%A8%E7%94%BB/">美化-网页主题切换的渐变动画</a>
          </li>
        
          <li>
            <a href="/2025/09/29/%E5%89%8D%E7%AB%AF/css/css-%E8%A1%A8%E6%A0%BCtd%E4%B8%8B%E9%AB%98%E5%BA%A6%E4%B8%A2%E5%A4%B1%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95/">css-表格td下高度丢失解决办法</a>
          </li>
        
          <li>
            <a href="/2025/09/23/%E5%89%8D%E7%AB%AF/vue/vue-%E5%B0%81%E8%A3%85%E5%87%BD%E6%95%B0%E5%BC%8F%E8%A7%A6%E5%8F%91%E5%8F%B3%E9%94%AE%E8%8F%9C%E5%8D%95/">vue-封装函数式触发右键菜单组件</a>
          </li>
        
          <li>
            <a href="/2025/06/04/%E5%89%8D%E7%AB%AF/UI%E5%BA%93/UI%E5%BA%93-ElTooltip%E5%8F%AA%E5%9C%A8%E6%96%87%E5%AD%97%E8%B6%85%E5%87%BA%E8%8C%83%E5%9B%B4%E6%97%B6%E6%98%BE%E7%A4%BA%E7%9A%84%E8%A7%86%E7%BA%BF%E6%96%B9%E5%BC%8F/">UI库-ElTooltip只在文字超出范围时显示的视线方式</a>
          </li>
        
          <li>
            <a href="/2025/05/29/%E5%89%8D%E7%AB%AF/UI%E5%BA%93/UI%E5%BA%93-%E5%B0%81%E8%A3%85PageTable%E6%80%BB%E8%A7%88/">UI库-封装PageTable总览</a>
          </li>
        
      </ul>
    </div>
  </div>

    
        
        <div class="nexmoe-widget-wrap">
    <div class="nexmoe-widget nexmoe-link">
		<ul>
        
            <li>
                <a href="https://tangyuxian.com/" target="_blank" >
                    <img src="https://cdn.tangyuxian.com/PicGoImages/202304272004917.jpg" alt="糖糖第一可爱"></img>
                    <p style="color: var(--color-primary)">糖糖第一可爱</p>
                </a>
            </li>
        
		</ul>
    </div>
</div>
<style>
.nexmoe-widget-wrap .nexmoe-link ul li a {
    text-align : center;
}
.nexmoe-widget-wrap .nexmoe-link ul li a img {
    max-width : 100%;
}
.nexmoe-widget-wrap .nexmoe-link ul li a p {
    margin: 10px 0;
}
</style>

    
   
    <div class="nexmoe-copyright">
        &copy; 2025 tangyuxian
        Powered by <a href="http://hexo.io/" target="_blank">Hexo</a>
        & <a href="https://github.com/tangyuxian/hexo-theme-tangyuxian" target="_blank">Tangyuxian</a>
        <br><a target="_blank" href="https://beian.miit.gov.cn/">辽ICP备2021002341号</a>
<div style="font-size: 12px">
   <script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>
   本站总访问量  <a id="busuanzi_value_site_pv"></a> 次<br />
   本站访客数<a id="busuanzi_value_site_uv"></a>人次
</div>

    </div>
</div><!-- .nexmoe-drawer --></div><div id="nexmoe-content"><div class="nexmoe-primary"><div class="nexmoe-post">
  <article>
    
        <div class="nexmoe-post-cover">
            <img src="/images/post/browser.jpg" alt="浏览器-高性能渲染十万条数据(虚拟列表)" loading="lazy">
            <h1>浏览器-高性能渲染十万条数据(虚拟列表)</h1>
        </div>
        
    
    <div class="nexmoe-post-meta">
    <div class="nexmoe-rainbow">
        <a class="nexmoefont icon-calendar-fill">2021年04月16日</a>
        
            <a class="nexmoefont icon-appstore-fill -link" href="/categories/%E5%89%8D%E7%AB%AF/">前端</a>
        
        
    </div>
    
    
    
    
    
</div>

    <p>对于一次性插入大量数据的情况，一般有两种做法：时间分片,虚拟列表</p>
<span id="more"></span>

<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>在工作中，有时会遇到需要一些不能使用分页方式来加载列表数据的业务情况，对于此，我们称这种列表叫做<code>长列表</code>。比如，在一些外汇交易系统中，前端会实时的展示用户的持仓情况(收益、亏损、手数等)，此时对于用户的持仓列表一般是不能分页的。</p>
<p>在<a href="/2021/04/16/%E5%89%8D%E7%AB%AF/%E6%B5%8F%E8%A7%88%E5%99%A8/%E6%B5%8F%E8%A7%88%E5%99%A8-%E9%AB%98%E6%80%A7%E8%83%BD%E6%B8%B2%E6%9F%93%E5%8D%81%E4%B8%87%E6%9D%A1%E6%95%B0%E6%8D%AE-%E6%97%B6%E9%97%B4%E5%88%86%E7%89%87/">高性能渲染十万条数据(时间分片)</a>一文中，提到了可以使用<code>时间分片</code>的方式来对长列表进行渲染，但这种方式更适用于列表项的DOM结构十分简单的情况。本文会介绍使用<code>虚拟列表</code>的方式，来同时加载大量数据。</p>
<h2 id="为什么需要使用虚拟列表"><a href="#为什么需要使用虚拟列表" class="headerlink" title="为什么需要使用虚拟列表"></a>为什么需要使用虚拟列表</h2><p>假设我们的长列表需要展示10000条记录，我们同时将10000条记录渲染到页面中，先来看看需要花费多长时间：</p>
<figure class="shiki html"><div class='codeblock'><div class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></div><div class="code"><pre class="shiki github-dark"><code><span class="line"><span style="color: #E1E4E8">&lt;</span><span style="color: #85E89D">button</span><span style="color: #E1E4E8"> </span><span style="color: #B392F0">id</span><span style="color: #E1E4E8">=</span><span style="color: #9ECBFF">&quot;button&quot;</span><span style="color: #E1E4E8">&gt;button&lt;/</span><span style="color: #85E89D">button</span><span style="color: #E1E4E8">&gt;&lt;</span><span style="color: #85E89D">br</span><span style="color: #E1E4E8">&gt;</span></span>
<span class="line"><span style="color: #E1E4E8">&lt;</span><span style="color: #85E89D">ul</span><span style="color: #E1E4E8"> </span><span style="color: #B392F0">id</span><span style="color: #E1E4E8">=</span><span style="color: #9ECBFF">&quot;container&quot;</span><span style="color: #E1E4E8">&gt;&lt;/</span><span style="color: #85E89D">ul</span><span style="color: #E1E4E8">&gt;</span></span></code></pre></div></div></figure>



<figure class="shiki javascript"><div class='codeblock'><div class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></div><div class="code"><pre class="shiki github-dark"><code><span class="line"><span style="color: #E1E4E8">document.</span><span style="color: #B392F0">getElementById</span><span style="color: #E1E4E8">(</span><span style="color: #9ECBFF">&#39;button&#39;</span><span style="color: #E1E4E8">).</span><span style="color: #B392F0">addEventListener</span><span style="color: #E1E4E8">(</span><span style="color: #9ECBFF">&#39;click&#39;</span><span style="color: #E1E4E8">,</span><span style="color: #F97583">function</span><span style="color: #E1E4E8">(){</span></span>
<span class="line"><span style="color: #E1E4E8">   </span><span style="color: #6A737D">// 记录任务开始时间</span></span>
<span class="line"><span style="color: #E1E4E8">   </span><span style="color: #F97583">let</span><span style="color: #E1E4E8"> now </span><span style="color: #F97583">=</span><span style="color: #E1E4E8"> Date.</span><span style="color: #B392F0">now</span><span style="color: #E1E4E8">();</span></span>
<span class="line"><span style="color: #E1E4E8">   </span><span style="color: #6A737D">// 插入一万条数据</span></span>
<span class="line"><span style="color: #E1E4E8">   </span><span style="color: #F97583">const</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">total</span><span style="color: #E1E4E8"> </span><span style="color: #F97583">=</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">10000</span><span style="color: #E1E4E8">;</span></span>
<span class="line"><span style="color: #E1E4E8">   </span><span style="color: #6A737D">// 获取容器</span></span>
<span class="line"><span style="color: #E1E4E8">   </span><span style="color: #F97583">let</span><span style="color: #E1E4E8"> ul </span><span style="color: #F97583">=</span><span style="color: #E1E4E8"> document.</span><span style="color: #B392F0">getElementById</span><span style="color: #E1E4E8">(</span><span style="color: #9ECBFF">&#39;container&#39;</span><span style="color: #E1E4E8">);</span></span>
<span class="line"><span style="color: #E1E4E8">   </span><span style="color: #6A737D">// 将数据插入容器中</span></span>
<span class="line"><span style="color: #E1E4E8">   </span><span style="color: #F97583">for</span><span style="color: #E1E4E8"> (</span><span style="color: #F97583">let</span><span style="color: #E1E4E8"> i </span><span style="color: #F97583">=</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">0</span><span style="color: #E1E4E8">; i </span><span style="color: #F97583">&lt;</span><span style="color: #E1E4E8"> total; i</span><span style="color: #F97583">++</span><span style="color: #E1E4E8">) {</span></span>
<span class="line"><span style="color: #E1E4E8">       </span><span style="color: #F97583">let</span><span style="color: #E1E4E8"> li </span><span style="color: #F97583">=</span><span style="color: #E1E4E8"> document.</span><span style="color: #B392F0">createElement</span><span style="color: #E1E4E8">(</span><span style="color: #9ECBFF">&#39;li&#39;</span><span style="color: #E1E4E8">);</span></span>
<span class="line"><span style="color: #E1E4E8">       li.innerText </span><span style="color: #F97583">=</span><span style="color: #E1E4E8"> </span><span style="color: #F97583">~~</span><span style="color: #E1E4E8">(Math.</span><span style="color: #B392F0">random</span><span style="color: #E1E4E8">() </span><span style="color: #F97583">*</span><span style="color: #E1E4E8"> total)</span></span>
<span class="line"><span style="color: #E1E4E8">       ul.</span><span style="color: #B392F0">appendChild</span><span style="color: #E1E4E8">(li);</span></span>
<span class="line"><span style="color: #E1E4E8">   }</span></span>
<span class="line"><span style="color: #E1E4E8">   console.</span><span style="color: #B392F0">log</span><span style="color: #E1E4E8">(</span><span style="color: #9ECBFF">&#39;JS运行时间：&#39;</span><span style="color: #E1E4E8">,Date.</span><span style="color: #B392F0">now</span><span style="color: #E1E4E8">() </span><span style="color: #F97583">-</span><span style="color: #E1E4E8"> now);</span></span>
<span class="line"><span style="color: #E1E4E8">   </span><span style="color: #B392F0">setTimeout</span><span style="color: #E1E4E8">(()</span><span style="color: #F97583">=&gt;</span><span style="color: #E1E4E8">{</span></span>
<span class="line"><span style="color: #E1E4E8">     console.</span><span style="color: #B392F0">log</span><span style="color: #E1E4E8">(</span><span style="color: #9ECBFF">&#39;总运行时间：&#39;</span><span style="color: #E1E4E8">,Date.</span><span style="color: #B392F0">now</span><span style="color: #E1E4E8">() </span><span style="color: #F97583">-</span><span style="color: #E1E4E8"> now);</span></span>
<span class="line"><span style="color: #E1E4E8">   },</span><span style="color: #79B8FF">0</span><span style="color: #E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #E1E4E8">   </span><span style="color: #6A737D">// print JS运行时间： 38</span></span>
<span class="line"><span style="color: #E1E4E8">   </span><span style="color: #6A737D">// print 总运行时间： 957 </span></span>
<span class="line"><span style="color: #E1E4E8"> })</span></span></code></pre></div></div></figure>

<p>当我们点击按钮，会同时向页面中加入一万条记录，通过控制台的输出，我们可以粗略的统计到，JS的运行时间为<code>38ms</code>,但渲染完成后的总时间为<code>957ms</code>。</p>
<p>简单说明一下，为何两次<code>console.log</code>的结果时间差异巨大，并且是如何简单来统计<code>JS运行时间</code>和<code>总渲染时间</code>：</p>
<ul>
<li>在 JS 的<code>Event Loop</code>中，当JS引擎所管理的执行栈中的事件以及所有微任务事件全部执行完后，才会触发渲染线程对页面进行渲染</li>
<li>第一个<code>console.log</code>的触发时间是在页面进行渲染之前，此时得到的间隔时间为JS运行所需要的时间</li>
<li>第二个<code>console.log</code>是放到 setTimeout 中的，它的触发时间是在渲染完成，在下一次<code>Event Loop</code>中执行的</li>
</ul>
<p><a target="_blank" rel="noopener" href="https://juejin.im/post/6844903919789801486">关于Event Loop的详细内容请参见这篇文章–&gt;</a></p>
<p>然后，我们通过<code>Chrome</code>的<code>Performance</code>工具来详细的分析这段代码的性能瓶颈在哪里：</p>
<p><img onerror="imgOnError(this);" data-fancybox="gallery" src="https://cdn.tangyuxian.com/PicGoImages/20210420102654.gif" alt="img" data-caption="img" loading="lazy"></p>
<p>从<code>Performance</code>可以看出，代码从执行到渲染结束，共消耗了<code>960.8ms</code>,其中的主要时间消耗如下：</p>
<ul>
<li>Event(click) : <code>40.84ms</code></li>
<li>Recalculate Style : <code>105.08ms</code></li>
<li>Layout : <code>731.56ms</code></li>
<li>Update Layer Tree : <code>58.87ms</code></li>
<li>Paint : <code>15.32ms</code></li>
</ul>
<p>从这里我们可以看出，我们的代码的执行过程中，消耗时间最多的两个阶段是<code>Recalculate Style</code>和<code>Layout</code>。</p>
<ul>
<li><code>Recalculate Style</code>：样式计算，浏览器根据css选择器计算哪些元素应该应用哪些规则，确定每个元素具体的样式。</li>
<li><code>Layout</code>：布局，知道元素应用哪些规则之后，浏览器开始计算它要占据的空间大小及其在屏幕的位置。</li>
</ul>
<p>在实际的工作中，列表项必然不会像例子中仅仅只由一个li标签组成，必然是由复杂DOM节点组成的。</p>
<p>那么可以想象的是，当列表项数过多并且列表项结构复杂的时候，同时渲染时，会在<code>Recalculate Style</code>和<code>Layout</code>阶段消耗大量的时间。</p>
<p>而<code>虚拟列表</code>就是解决这一问题的一种实现。</p>
<h2 id="什么是虚拟列表"><a href="#什么是虚拟列表" class="headerlink" title="什么是虚拟列表"></a>什么是虚拟列表</h2><p><code>虚拟列表</code>其实是按需显示的一种实现，即只对<code>可见区域</code>进行渲染，对<code>非可见区域</code>中的数据不渲染或部分渲染的技术，从而达到极高的渲染性能。</p>
<p>假设有1万条记录需要同时渲染，我们屏幕的<code>可见区域</code>的高度为<code>500px</code>,而列表项的高度为<code>50px</code>，则此时我们在屏幕中最多只能看到10个列表项，那么在首次渲染的时候，我们只需加载10条即可。</p>
<p><img onerror="imgOnError(this);" data-fancybox="gallery" src="https://user-gold-cdn.xitu.io/2019/10/29/16e15195cf16a558?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img" data-caption="img" loading="lazy"></p>
<p>说完首次加载，再分析一下当滚动发生时，我们可以通过计算当前滚动值得知此时在屏幕<code>可见区域</code>应该显示的列表项。</p>
<p>假设滚动发生，滚动条距顶部的位置为<code>150px</code>,则我们可得知在<code>可见区域</code>内的列表项为<code>第4项</code>至&#96;第13项。</p>
<p><img onerror="imgOnError(this);" data-fancybox="gallery" src="https://user-gold-cdn.xitu.io/2019/10/29/16e15197c273cbd9?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img" data-caption="img" loading="lazy"></p>
<h2 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h2><p>虚拟列表的实现，实际上就是在首屏加载的时候，只加载<code>可视区域</code>内需要的列表项，当滚动发生时，动态通过计算获得<code>可视区域</code>内的列表项，并将<code>非可视区域</code>内存在的列表项删除。</p>
<ul>
<li>计算当前<code>可视区域</code>起始数据索引(<code>startIndex</code>)</li>
<li>计算当前<code>可视区域</code>结束数据索引(<code>endIndex</code>)</li>
<li>计算当前<code>可视区域的</code>数据，并渲染到页面中</li>
<li>计算<code>startIndex</code>对应的数据在整个列表中的偏移位置<code>startOffset</code>并设置到列表上</li>
</ul>
<p><img onerror="imgOnError(this);" data-fancybox="gallery" src="https://user-gold-cdn.xitu.io/2019/10/29/16e1519a393dee2c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img" data-caption="img" loading="lazy"></p>
<p>由于只是对<code>可视区域</code>内的列表项进行渲染，所以为了保持列表容器的高度并可正常的触发滚动，将Html结构设计成如下结构：</p>
<figure class="shiki html"><div class='codeblock'><div class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></div><div class="code"><pre class="shiki github-dark"><code><span class="line"><span style="color: #E1E4E8">&lt;</span><span style="color: #85E89D">div</span><span style="color: #E1E4E8"> </span><span style="color: #B392F0">class</span><span style="color: #E1E4E8">=</span><span style="color: #9ECBFF">&quot;infinite-list-container&quot;</span><span style="color: #E1E4E8">&gt;</span></span>
<span class="line"><span style="color: #E1E4E8">    &lt;</span><span style="color: #85E89D">div</span><span style="color: #E1E4E8"> </span><span style="color: #B392F0">class</span><span style="color: #E1E4E8">=</span><span style="color: #9ECBFF">&quot;infinite-list-phantom&quot;</span><span style="color: #E1E4E8">&gt;&lt;/</span><span style="color: #85E89D">div</span><span style="color: #E1E4E8">&gt;</span></span>
<span class="line"><span style="color: #E1E4E8">    &lt;</span><span style="color: #85E89D">div</span><span style="color: #E1E4E8"> </span><span style="color: #B392F0">class</span><span style="color: #E1E4E8">=</span><span style="color: #9ECBFF">&quot;infinite-list&quot;</span><span style="color: #E1E4E8">&gt;</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #6A737D">&lt;!-- item-1 --&gt;</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #6A737D">&lt;!-- item-2 --&gt;</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #6A737D">&lt;!-- ...... --&gt;</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #6A737D">&lt;!-- item-n --&gt;</span></span>
<span class="line"><span style="color: #E1E4E8">    &lt;/</span><span style="color: #85E89D">div</span><span style="color: #E1E4E8">&gt;</span></span>
<span class="line"><span style="color: #E1E4E8">&lt;/</span><span style="color: #85E89D">div</span><span style="color: #E1E4E8">&gt;</span></span></code></pre></div></div></figure>

<ul>
<li><code>infinite-list-container</code> 为<code>可视区域</code>的容器</li>
<li><code>infinite-list-phantom</code> 为容器内的占位，高度为总列表高度，用于形成滚动条</li>
<li><code>infinite-list</code> 为列表项的<code>渲染区域</code></li>
</ul>
<p>接着，监听<code>infinite-list-container</code>的<code>scroll</code>事件，获取滚动位置<code>scrollTop</code></p>
<ul>
<li>假定<code>可视区域</code>高度固定，称之为<code>screenHeight</code></li>
<li>假定<code>列表每项</code>高度固定，称之为<code>itemSize</code></li>
<li>假定<code>列表数据</code>称之为<code>listData</code></li>
<li>假定<code>当前滚动位置</code>称之为<code>scrollTop</code></li>
</ul>
<p>则可推算出：</p>
<ul>
<li>列表总高度<code>listHeight</code> &#x3D; listData.length * itemSize</li>
<li>可显示的列表项数<code>visibleCount</code> &#x3D; Math.ceil(screenHeight &#x2F; itemSize)</li>
<li>数据的起始索引<code>startIndex</code> &#x3D; Math.floor(scrollTop &#x2F; itemSize)</li>
<li>数据的结束索引<code>endIndex</code> &#x3D; startIndex + visibleCount</li>
<li>列表显示数据为<code>visibleData</code> &#x3D; listData.slice(startIndex,endIndex)</li>
</ul>
<p>当滚动后，由于<code>渲染区域</code>相对于<code>可视区域</code>已经发生了偏移，此时我需要获取一个偏移量<code>startOffset</code>，通过样式控制将<code>渲染区域</code>偏移至<code>可视区域</code>中。</p>
<ul>
<li>偏移量<code>startOffset</code> &#x3D; scrollTop - (scrollTop % itemSize);</li>
</ul>
<p>最终的<code>简易代码</code>如下：</p>
<figure class="shiki vue"><div class='codeblock'><div class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br></pre></div><div class="code"><pre class="shiki github-dark"><code><span class="line"><span style="color: #E1E4E8">&lt;</span><span style="color: #85E89D">template</span><span style="color: #E1E4E8">&gt;</span></span>
<span class="line"><span style="color: #E1E4E8">  &lt;</span><span style="color: #85E89D">div</span><span style="color: #E1E4E8"> </span><span style="color: #B392F0">ref</span><span style="color: #E1E4E8">=</span><span style="color: #9ECBFF">&quot;list&quot;</span><span style="color: #E1E4E8"> </span><span style="color: #B392F0">class</span><span style="color: #E1E4E8">=</span><span style="color: #9ECBFF">&quot;infinite-list-container&quot;</span><span style="color: #E1E4E8"> @</span><span style="color: #B392F0">scroll</span><span style="color: #E1E4E8">=</span><span style="color: #9ECBFF">&quot;</span><span style="color: #B392F0">scrollEvent</span><span style="color: #E1E4E8">($event)</span><span style="color: #9ECBFF">&quot;</span><span style="color: #E1E4E8">&gt;</span></span>
<span class="line"><span style="color: #E1E4E8">    &lt;</span><span style="color: #85E89D">div</span><span style="color: #E1E4E8"> </span><span style="color: #B392F0">class</span><span style="color: #E1E4E8">=</span><span style="color: #9ECBFF">&quot;infinite-list-phantom&quot;</span><span style="color: #E1E4E8"> :</span><span style="color: #B392F0">style</span><span style="color: #E1E4E8">=</span><span style="color: #9ECBFF">&quot;</span><span style="color: #E1E4E8">{ </span><span style="color: #B392F0">height</span><span style="color: #E1E4E8">: listHeight </span><span style="color: #F97583">+</span><span style="color: #E1E4E8"> </span><span style="color: #9ECBFF">&#39;px&#39;</span><span style="color: #E1E4E8"> }</span><span style="color: #9ECBFF">&quot;</span><span style="color: #E1E4E8">&gt;&lt;/</span><span style="color: #85E89D">div</span><span style="color: #E1E4E8">&gt;</span></span>
<span class="line"><span style="color: #E1E4E8">    &lt;</span><span style="color: #85E89D">div</span><span style="color: #E1E4E8"> </span><span style="color: #B392F0">class</span><span style="color: #E1E4E8">=</span><span style="color: #9ECBFF">&quot;infinite-list&quot;</span><span style="color: #E1E4E8"> :</span><span style="color: #B392F0">style</span><span style="color: #E1E4E8">=</span><span style="color: #9ECBFF">&quot;</span><span style="color: #E1E4E8">{ </span><span style="color: #B392F0">transform</span><span style="color: #E1E4E8">: getTransform }</span><span style="color: #9ECBFF">&quot;</span><span style="color: #E1E4E8">&gt;</span></span>
<span class="line"><span style="color: #E1E4E8">      &lt;</span><span style="color: #85E89D">div</span><span style="color: #E1E4E8"> </span><span style="color: #B392F0">ref</span><span style="color: #E1E4E8">=</span><span style="color: #9ECBFF">&quot;items&quot;</span></span>
<span class="line"><span style="color: #E1E4E8">        </span><span style="color: #B392F0">class</span><span style="color: #E1E4E8">=</span><span style="color: #9ECBFF">&quot;infinite-list-item&quot;</span></span>
<span class="line"><span style="color: #E1E4E8">        </span><span style="color: #F97583">v-for</span><span style="color: #E1E4E8">=</span><span style="color: #9ECBFF">&quot;</span><span style="color: #E1E4E8">item </span><span style="color: #F97583">in</span><span style="color: #E1E4E8"> visibleData</span><span style="color: #9ECBFF">&quot;</span></span>
<span class="line"><span style="color: #E1E4E8">        :</span><span style="color: #B392F0">key</span><span style="color: #E1E4E8">=</span><span style="color: #9ECBFF">&quot;</span><span style="color: #E1E4E8">item.id</span><span style="color: #9ECBFF">&quot;</span></span>
<span class="line"><span style="color: #E1E4E8">        :</span><span style="color: #B392F0">style</span><span style="color: #E1E4E8">=</span><span style="color: #9ECBFF">&quot;</span><span style="color: #E1E4E8">{ </span><span style="color: #B392F0">height</span><span style="color: #E1E4E8">: itemSize </span><span style="color: #F97583">+</span><span style="color: #E1E4E8"> </span><span style="color: #9ECBFF">&#39;px&#39;</span><span style="color: #E1E4E8">,</span><span style="color: #B392F0">lineHeight</span><span style="color: #E1E4E8">: itemSize </span><span style="color: #F97583">+</span><span style="color: #E1E4E8"> </span><span style="color: #9ECBFF">&#39;px&#39;</span><span style="color: #E1E4E8"> }</span><span style="color: #9ECBFF">&quot;</span></span>
<span class="line"><span style="color: #E1E4E8">      &gt;{{ item.value }}&lt;/</span><span style="color: #85E89D">div</span><span style="color: #E1E4E8">&gt;</span></span>
<span class="line"><span style="color: #E1E4E8">    &lt;/</span><span style="color: #85E89D">div</span><span style="color: #E1E4E8">&gt;</span></span>
<span class="line"><span style="color: #E1E4E8">  &lt;/</span><span style="color: #85E89D">div</span><span style="color: #E1E4E8">&gt;</span></span>
<span class="line"><span style="color: #E1E4E8">&lt;/</span><span style="color: #85E89D">template</span><span style="color: #E1E4E8">&gt;</span></span>
<span class="line"><span style="color: #E1E4E8">&lt;</span><span style="color: #85E89D">script</span><span style="color: #E1E4E8">&gt;</span></span>
<span class="line"><span style="color: #F97583">export</span><span style="color: #E1E4E8"> </span><span style="color: #F97583">default</span><span style="color: #E1E4E8"> {</span></span>
<span class="line"><span style="color: #E1E4E8">  name:</span><span style="color: #9ECBFF">&#39;VirtualList&#39;</span><span style="color: #E1E4E8">,</span></span>
<span class="line"><span style="color: #E1E4E8">  props: {</span></span>
<span class="line"><span style="color: #E1E4E8">    </span><span style="color: #6A737D">//所有列表数据</span></span>
<span class="line"><span style="color: #E1E4E8">    listData:{</span></span>
<span class="line"><span style="color: #E1E4E8">      type:Array,</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #B392F0">default</span><span style="color: #E1E4E8">:()</span><span style="color: #F97583">=&gt;</span><span style="color: #E1E4E8">[]</span></span>
<span class="line"><span style="color: #E1E4E8">    },</span></span>
<span class="line"><span style="color: #E1E4E8">    </span><span style="color: #6A737D">//每项高度</span></span>
<span class="line"><span style="color: #E1E4E8">    itemSize: {</span></span>
<span class="line"><span style="color: #E1E4E8">      type: Number,</span></span>
<span class="line"><span style="color: #E1E4E8">      default:</span><span style="color: #79B8FF">200</span></span>
<span class="line"><span style="color: #E1E4E8">    }</span></span>
<span class="line"><span style="color: #E1E4E8">  },</span></span>
<span class="line"><span style="color: #E1E4E8">  computed:{</span></span>
<span class="line"><span style="color: #E1E4E8">    </span><span style="color: #6A737D">//列表总高度</span></span>
<span class="line"><span style="color: #E1E4E8">    </span><span style="color: #B392F0">listHeight</span><span style="color: #E1E4E8">(){</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #F97583">return</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.listData.</span><span style="color: #79B8FF">length</span><span style="color: #E1E4E8"> </span><span style="color: #F97583">*</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.itemSize;</span></span>
<span class="line"><span style="color: #E1E4E8">    },</span></span>
<span class="line"><span style="color: #E1E4E8">    </span><span style="color: #6A737D">//可显示的列表项数</span></span>
<span class="line"><span style="color: #E1E4E8">    </span><span style="color: #B392F0">visibleCount</span><span style="color: #E1E4E8">(){</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #F97583">return</span><span style="color: #E1E4E8"> Math.</span><span style="color: #B392F0">ceil</span><span style="color: #E1E4E8">(</span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.screenHeight </span><span style="color: #F97583">/</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.itemSize)</span></span>
<span class="line"><span style="color: #E1E4E8">    },</span></span>
<span class="line"><span style="color: #E1E4E8">    </span><span style="color: #6A737D">//偏移量对应的style</span></span>
<span class="line"><span style="color: #E1E4E8">    </span><span style="color: #B392F0">getTransform</span><span style="color: #E1E4E8">(){</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #F97583">return</span><span style="color: #E1E4E8"> </span><span style="color: #9ECBFF">`translate3d(0,${</span><span style="color: #79B8FF">this</span><span style="color: #9ECBFF">.</span><span style="color: #E1E4E8">startOffset</span><span style="color: #9ECBFF">}px,0)`</span><span style="color: #E1E4E8">;</span></span>
<span class="line"><span style="color: #E1E4E8">    },</span></span>
<span class="line"><span style="color: #E1E4E8">    </span><span style="color: #6A737D">//获取真实显示列表数据</span></span>
<span class="line"><span style="color: #E1E4E8">    </span><span style="color: #B392F0">visibleData</span><span style="color: #E1E4E8">(){</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #F97583">return</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.listData.</span><span style="color: #B392F0">slice</span><span style="color: #E1E4E8">(</span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.start, Math.</span><span style="color: #B392F0">min</span><span style="color: #E1E4E8">(</span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.end,</span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.listData.</span><span style="color: #79B8FF">length</span><span style="color: #E1E4E8">));</span></span>
<span class="line"><span style="color: #E1E4E8">    }</span></span>
<span class="line"><span style="color: #E1E4E8">  },</span></span>
<span class="line"><span style="color: #E1E4E8">  </span><span style="color: #B392F0">mounted</span><span style="color: #E1E4E8">() {</span></span>
<span class="line"><span style="color: #E1E4E8">    </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.screenHeight </span><span style="color: #F97583">=</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.$el.clientHeight;</span></span>
<span class="line"><span style="color: #E1E4E8">    </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.start </span><span style="color: #F97583">=</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">0</span><span style="color: #E1E4E8">;</span></span>
<span class="line"><span style="color: #E1E4E8">    </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.end </span><span style="color: #F97583">=</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.start </span><span style="color: #F97583">+</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.visibleCount;</span></span>
<span class="line"><span style="color: #E1E4E8">  },</span></span>
<span class="line"><span style="color: #E1E4E8">  </span><span style="color: #B392F0">data</span><span style="color: #E1E4E8">() {</span></span>
<span class="line"><span style="color: #E1E4E8">    </span><span style="color: #F97583">return</span><span style="color: #E1E4E8"> {</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #6A737D">//可视区域高度</span></span>
<span class="line"><span style="color: #E1E4E8">      screenHeight:</span><span style="color: #79B8FF">0</span><span style="color: #E1E4E8">,</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #6A737D">//偏移量</span></span>
<span class="line"><span style="color: #E1E4E8">      startOffset:</span><span style="color: #79B8FF">0</span><span style="color: #E1E4E8">,</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #6A737D">//起始索引</span></span>
<span class="line"><span style="color: #E1E4E8">      start:</span><span style="color: #79B8FF">0</span><span style="color: #E1E4E8">,</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #6A737D">//结束索引</span></span>
<span class="line"><span style="color: #E1E4E8">      end:</span><span style="color: #79B8FF">null</span><span style="color: #E1E4E8">,</span></span>
<span class="line"><span style="color: #E1E4E8">    };</span></span>
<span class="line"><span style="color: #E1E4E8">  },</span></span>
<span class="line"><span style="color: #E1E4E8">  methods: {</span></span>
<span class="line"><span style="color: #E1E4E8">    </span><span style="color: #B392F0">scrollEvent</span><span style="color: #E1E4E8">() {</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #6A737D">//当前滚动位置</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #F97583">let</span><span style="color: #E1E4E8"> scrollTop </span><span style="color: #F97583">=</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.$refs.list.scrollTop;</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #6A737D">//此时的开始索引</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.start </span><span style="color: #F97583">=</span><span style="color: #E1E4E8"> Math.</span><span style="color: #B392F0">floor</span><span style="color: #E1E4E8">(scrollTop </span><span style="color: #F97583">/</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.itemSize);</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #6A737D">//此时的结束索引</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.end </span><span style="color: #F97583">=</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.start </span><span style="color: #F97583">+</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.visibleCount;</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #6A737D">//此时的偏移量</span></span>
<span class="line"><span style="color: #E1E4E8">      </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.startOffset </span><span style="color: #F97583">=</span><span style="color: #E1E4E8"> scrollTop </span><span style="color: #F97583">-</span><span style="color: #E1E4E8"> (scrollTop </span><span style="color: #F97583">%</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.itemSize);</span></span>
<span class="line"><span style="color: #E1E4E8">    }</span></span>
<span class="line"><span style="color: #E1E4E8">  }</span></span>
<span class="line"><span style="color: #E1E4E8">};</span></span>
<span class="line"><span style="color: #E1E4E8">&lt;/</span><span style="color: #85E89D">script</span><span style="color: #E1E4E8">&gt;</span></span></code></pre></div></div></figure>

<p><a target="_blank" rel="noopener" href="https://codesandbox.io/s/virtuallist-1-rp8pi">点击查看在线DEMO及完整代码</a></p>
<p>最终效果如下：</p>
<p><img onerror="imgOnError(this);" data-fancybox="gallery" src="https://user-gold-cdn.xitu.io/2019/10/29/16e151e017d7bba3?imageslim" alt="img" data-caption="img" loading="lazy"></p>
<h2 id="列表项动态高度"><a href="#列表项动态高度" class="headerlink" title="列表项动态高度"></a>列表项动态高度</h2><p>在之前的实现中，列表项的高度是固定的，因为高度固定，所以可以很轻易的获取列表项的整体高度以及滚动时的显示数据与对应的偏移量。而实际应用的时候，当列表中包含文本之类的可变内容，会导致列表项的高度并不相同。</p>
<p>比如这种情况：</p>
<p><img onerror="imgOnError(this);" data-fancybox="gallery" src="https://user-gold-cdn.xitu.io/2019/10/29/16e1519f1e121be9?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img" data-caption="img" loading="lazy"></p>
<p>在虚拟列表中应用动态高度的解决方案一般有如下三种：</p>
<blockquote>
<p>1.对组件属性<code>itemSize</code>进行扩展，支持传递类型为<code>数字</code>、<code>数组</code>、<code>函数</code></p>
</blockquote>
<ul>
<li>可以是一个固定值，如 100，此时列表项是固高的</li>
<li>可以是一个包含所有列表项高度的数据，如 [50, 20, 100, 80, …]</li>
<li>可以是一个根据列表项索引返回其高度的函数：(index: number): number</li>
</ul>
<p>这种方式虽然有比较好的灵活度，但仅适用于可以预先知道或可以通过计算得知列表项高度的情况，依然无法解决列表项高度由内容撑开的情况。</p>
<blockquote>
<p>2.将列表项<code>渲染到屏幕外</code>，对其高度进行测量并缓存，然后再将其渲染至可视区域内。</p>
</blockquote>
<p>由于预先渲染至屏幕外，再渲染至屏幕内，这导致渲染成本增加一倍，这对于数百万用户在低端移动设备上使用的产品来说是不切实际的。</p>
<blockquote>
<p>3.以<code>预估高度</code>先行渲染，然后获取真实高度并缓存。</p>
</blockquote>
<p>这是我选择的实现方式，可以避免前两种方案的不足。</p>
<p>接下来，来看如何简易的实现：</p>
<p>定义组件属性<code>estimatedItemSize</code>,用于接收<code>预估高度</code></p>
<figure class="shiki"><div class='codeblock'><div class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></div><div class="code"><pre class="shiki github-dark"><code><span class="line"><span style="color: #e1e4e8">props: {</span></span>
<span class="line"><span style="color: #e1e4e8">  //预估高度</span></span>
<span class="line"><span style="color: #e1e4e8">  estimatedItemSize:{</span></span>
<span class="line"><span style="color: #e1e4e8">    type:Number</span></span>
<span class="line"><span style="color: #e1e4e8">  }</span></span>
<span class="line"><span style="color: #e1e4e8">}</span></span>
<span class="line"><span style="color: #e1e4e8">复制代码</span></span></code></pre></div></div></figure>

<p>定义<code>positions</code>，用于列表项渲染后存储<code>每一项的高度以及位置</code>信息，</p>
<figure class="shiki"><div class='codeblock'><div class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></div><div class="code"><pre class="shiki github-dark"><code><span class="line"><span style="color: #e1e4e8">this.positions = [</span></span>
<span class="line"><span style="color: #e1e4e8">  // {</span></span>
<span class="line"><span style="color: #e1e4e8">  //   top:0,</span></span>
<span class="line"><span style="color: #e1e4e8">  //   bottom:100,</span></span>
<span class="line"><span style="color: #e1e4e8">  //   height:100</span></span>
<span class="line"><span style="color: #e1e4e8">  // }</span></span>
<span class="line"><span style="color: #e1e4e8">];</span></span>
<span class="line"><span style="color: #e1e4e8">复制代码</span></span></code></pre></div></div></figure>

<p>并在初始时根据<code>estimatedItemSize</code>对<code>positions</code>进行初始化。</p>
<figure class="shiki"><div class='codeblock'><div class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></div><div class="code"><pre class="shiki github-dark"><code><span class="line"><span style="color: #e1e4e8">initPositions(){</span></span>
<span class="line"><span style="color: #e1e4e8">  this.positions = this.listData.map((item,index)=&gt;{</span></span>
<span class="line"><span style="color: #e1e4e8">    return {</span></span>
<span class="line"><span style="color: #e1e4e8">      index,</span></span>
<span class="line"><span style="color: #e1e4e8">      height:this.estimatedItemSize,</span></span>
<span class="line"><span style="color: #e1e4e8">      top:index * this.estimatedItemSize,</span></span>
<span class="line"><span style="color: #e1e4e8">      bottom:(index + 1) * this.estimatedItemSize</span></span>
<span class="line"><span style="color: #e1e4e8">    }</span></span>
<span class="line"><span style="color: #e1e4e8">  })</span></span>
<span class="line"><span style="color: #e1e4e8">}</span></span>
<span class="line"><span style="color: #e1e4e8">复制代码</span></span></code></pre></div></div></figure>

<p>由于列表项高度不定，并且我们维护了<code>positions</code>，用于记录每一项的位置，而<code>列表高度</code>实际就等于列表中最后一项的底部距离列表顶部的位置。</p>
<figure class="shiki"><div class='codeblock'><div class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></div><div class="code"><pre class="shiki github-dark"><code><span class="line"><span style="color: #e1e4e8">//列表总高度</span></span>
<span class="line"><span style="color: #e1e4e8">listHeight(){</span></span>
<span class="line"><span style="color: #e1e4e8">  return this.positions[this.positions.length - 1].bottom;</span></span>
<span class="line"><span style="color: #e1e4e8">}</span></span>
<span class="line"><span style="color: #e1e4e8">复制代码</span></span></code></pre></div></div></figure>

<p>由于需要在<code>渲染完成</code>后，获取列表每项的位置信息并缓存，所以使用钩子函数<code>updated</code>来实现：</p>
<figure class="shiki"><div class='codeblock'><div class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></div><div class="code"><pre class="shiki github-dark"><code><span class="line"><span style="color: #e1e4e8">updated(){</span></span>
<span class="line"><span style="color: #e1e4e8">  let nodes = this.$refs.items;</span></span>
<span class="line"><span style="color: #e1e4e8">  nodes.forEach((node)=&gt;{</span></span>
<span class="line"><span style="color: #e1e4e8">    let rect = node.getBoundingClientRect();</span></span>
<span class="line"><span style="color: #e1e4e8">    let height = rect.height;</span></span>
<span class="line"><span style="color: #e1e4e8">    let index = +node.id.slice(1)</span></span>
<span class="line"><span style="color: #e1e4e8">    let oldHeight = this.positions[index].height;</span></span>
<span class="line"><span style="color: #e1e4e8">    let dValue = oldHeight - height;</span></span>
<span class="line"><span style="color: #e1e4e8">    //存在差值</span></span>
<span class="line"><span style="color: #e1e4e8">    if(dValue){</span></span>
<span class="line"><span style="color: #e1e4e8">      this.positions[index].bottom = this.positions[index].bottom - dValue;</span></span>
<span class="line"><span style="color: #e1e4e8">      this.positions[index].height = height;</span></span>
<span class="line"><span style="color: #e1e4e8">      for(let k = index + 1;k&lt;this.positions.length; k++){</span></span>
<span class="line"><span style="color: #e1e4e8">        this.positions[k].top = this.positions[k-1].bottom;</span></span>
<span class="line"><span style="color: #e1e4e8">        this.positions[k].bottom = this.positions[k].bottom - dValue;</span></span>
<span class="line"><span style="color: #e1e4e8">      }</span></span>
<span class="line"><span style="color: #e1e4e8">    }</span></span>
<span class="line"><span style="color: #e1e4e8">  })</span></span>
<span class="line"><span style="color: #e1e4e8">}</span></span>
<span class="line"><span style="color: #e1e4e8">复制代码</span></span></code></pre></div></div></figure>

<p>滚动后获取列表<code>开始索引</code>的方法修改为通过<code>缓存</code>获取：</p>
<figure class="shiki"><div class='codeblock'><div class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></div><div class="code"><pre class="shiki github-dark"><code><span class="line"><span style="color: #e1e4e8">//获取列表起始索引</span></span>
<span class="line"><span style="color: #e1e4e8">getStartIndex(scrollTop = 0){</span></span>
<span class="line"><span style="color: #e1e4e8">  let item = this.positions.find(i =&gt; i &amp;&amp; i.bottom &gt; scrollTop);</span></span>
<span class="line"><span style="color: #e1e4e8">  return item.index;</span></span>
<span class="line"><span style="color: #e1e4e8">}</span></span>
<span class="line"><span style="color: #e1e4e8">复制代码</span></span></code></pre></div></div></figure>

<p>由于我们的缓存数据，本身就是有顺序的，所以获取<code>开始索引</code>的方法可以考虑通过<code>二分查找</code>的方式来降低检索次数：</p>
<figure class="shiki"><div class='codeblock'><div class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></div><div class="code"><pre class="shiki github-dark"><code><span class="line"><span style="color: #e1e4e8">//获取列表起始索引</span></span>
<span class="line"><span style="color: #e1e4e8">getStartIndex(scrollTop = 0){</span></span>
<span class="line"><span style="color: #e1e4e8">  //二分法查找</span></span>
<span class="line"><span style="color: #e1e4e8">  return this.binarySearch(this.positions,scrollTop)</span></span>
<span class="line"><span style="color: #e1e4e8">},</span></span>
<span class="line"><span style="color: #e1e4e8">//二分法查找</span></span>
<span class="line"><span style="color: #e1e4e8">binarySearch(list,value){</span></span>
<span class="line"><span style="color: #e1e4e8">  let start = 0;</span></span>
<span class="line"><span style="color: #e1e4e8">  let end = list.length - 1;</span></span>
<span class="line"><span style="color: #e1e4e8">  let tempIndex = null;</span></span>
<span class="line"><span style="color: #e1e4e8">  while(start &lt;= end){</span></span>
<span class="line"><span style="color: #e1e4e8">    let midIndex = parseInt((start + end)/2);</span></span>
<span class="line"><span style="color: #e1e4e8">    let midValue = list[midIndex].bottom;</span></span>
<span class="line"><span style="color: #e1e4e8">    if(midValue === value){</span></span>
<span class="line"><span style="color: #e1e4e8">      return midIndex + 1;</span></span>
<span class="line"><span style="color: #e1e4e8">    }else if(midValue &lt; value){</span></span>
<span class="line"><span style="color: #e1e4e8">      start = midIndex + 1;</span></span>
<span class="line"><span style="color: #e1e4e8">    }else if(midValue &gt; value){</span></span>
<span class="line"><span style="color: #e1e4e8">      if(tempIndex === null || tempIndex &gt; midIndex){</span></span>
<span class="line"><span style="color: #e1e4e8">        tempIndex = midIndex;</span></span>
<span class="line"><span style="color: #e1e4e8">      }</span></span>
<span class="line"><span style="color: #e1e4e8">      end = end - 1;</span></span>
<span class="line"><span style="color: #e1e4e8">    }</span></span>
<span class="line"><span style="color: #e1e4e8">  }</span></span>
<span class="line"><span style="color: #e1e4e8">  return tempIndex;</span></span>
<span class="line"><span style="color: #e1e4e8">},</span></span>
<span class="line"><span style="color: #e1e4e8">复制代码</span></span></code></pre></div></div></figure>

<p>滚动后将<code>偏移量</code>的获取方式变更：</p>
<figure class="shiki"><div class='codeblock'><div class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></div><div class="code"><pre class="shiki github-dark"><code><span class="line"><span style="color: #e1e4e8">scrollEvent() {</span></span>
<span class="line"><span style="color: #e1e4e8">  //...省略</span></span>
<span class="line"><span style="color: #e1e4e8">  if(this.start &gt;= 1){</span></span>
<span class="line"><span style="color: #e1e4e8">    this.startOffset = this.positions[this.start - 1].bottom</span></span>
<span class="line"><span style="color: #e1e4e8">  }else{</span></span>
<span class="line"><span style="color: #e1e4e8">    this.startOffset = 0;</span></span>
<span class="line"><span style="color: #e1e4e8">  }</span></span>
<span class="line"><span style="color: #e1e4e8">}</span></span>
<span class="line"><span style="color: #e1e4e8">复制代码</span></span></code></pre></div></div></figure>

<p>通过<a target="_blank" rel="noopener" href="https://github.com/marak/Faker.js/">faker.js</a> 来创建一些<code>随机数据</code></p>
<figure class="shiki"><div class='codeblock'><div class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></div><div class="code"><pre class="shiki github-dark"><code><span class="line"><span style="color: #e1e4e8">let data = [];</span></span>
<span class="line"><span style="color: #e1e4e8">for (let id = 0; id &lt; 10000; id++) {</span></span>
<span class="line"><span style="color: #e1e4e8">  data.push({</span></span>
<span class="line"><span style="color: #e1e4e8">    id,</span></span>
<span class="line"><span style="color: #e1e4e8">    value: faker.lorem.sentences() // 长文本</span></span>
<span class="line"><span style="color: #e1e4e8">  })</span></span>
<span class="line"><span style="color: #e1e4e8">}</span></span>
<span class="line"><span style="color: #e1e4e8">复制代码</span></span></code></pre></div></div></figure>

<p><a target="_blank" rel="noopener" href="https://codesandbox.io/s/virtuallist2-1bqk6">点击查看在线DEMO及完整代码</a></p>
<p>最终效果如下：</p>
<p><img onerror="imgOnError(this);" data-fancybox="gallery" src="https://user-gold-cdn.xitu.io/2019/10/29/16e151e96584b690?imageslim" alt="img" data-caption="img" loading="lazy"></p>
<p>从演示效果上看，我们实现了基于<code>文字内容动态撑高列表项</code>情况下的<code>虚拟列表</code>，但是我们可能会发现，当滚动过快时，会出现短暂的<code>白屏现象</code>。</p>
<p>为了使页面平滑滚动，我们还需要在<code>可见区域</code>的上方和下方渲染额外的项目，在滚动时给予一些<code>缓冲</code>，所以将屏幕分为三个区域：</p>
<ul>
<li>可视区域上方：<code>above</code></li>
<li>可视区域：<code>screen</code></li>
<li>可视区域下方：<code>below</code></li>
</ul>
<p><img onerror="imgOnError(this);" data-fancybox="gallery" src="https://user-gold-cdn.xitu.io/2019/10/29/16e151a59317cae7?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="img" data-caption="img" loading="lazy"></p>
<p>定义组件属性<code>bufferScale</code>,用于接收<code>缓冲区数据</code>与<code>可视区数据</code>的<code>比例</code></p>
<figure class="shiki"><div class='codeblock'><div class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></div><div class="code"><pre class="shiki github-dark"><code><span class="line"><span style="color: #e1e4e8">props: {</span></span>
<span class="line"><span style="color: #e1e4e8">  //缓冲区比例</span></span>
<span class="line"><span style="color: #e1e4e8">  bufferScale:{</span></span>
<span class="line"><span style="color: #e1e4e8">    type:Number,</span></span>
<span class="line"><span style="color: #e1e4e8">    default:1</span></span>
<span class="line"><span style="color: #e1e4e8">  }</span></span>
<span class="line"><span style="color: #e1e4e8">}</span></span>
<span class="line"><span style="color: #e1e4e8">复制代码</span></span></code></pre></div></div></figure>

<p>可视区上方渲染条数<code>aboveCount</code>获取方式如下：</p>
<figure class="shiki"><div class='codeblock'><div class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></div><div class="code"><pre class="shiki github-dark"><code><span class="line"><span style="color: #e1e4e8">aboveCount(){</span></span>
<span class="line"><span style="color: #e1e4e8">  return Math.min(this.start,this.bufferScale * this.visibleCount)</span></span>
<span class="line"><span style="color: #e1e4e8">}</span></span>
<span class="line"><span style="color: #e1e4e8">复制代码</span></span></code></pre></div></div></figure>

<p>可视区下方渲染条数<code>belowCount</code>获取方式如下：</p>
<figure class="shiki"><div class='codeblock'><div class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></div><div class="code"><pre class="shiki github-dark"><code><span class="line"><span style="color: #e1e4e8">belowCount(){</span></span>
<span class="line"><span style="color: #e1e4e8">  return Math.min(this.listData.length - this.end,this.bufferScale * this.visibleCount);</span></span>
<span class="line"><span style="color: #e1e4e8">}</span></span>
<span class="line"><span style="color: #e1e4e8">复制代码</span></span></code></pre></div></div></figure>

<p>真实渲染数据<code>visibleData</code>获取方式如下：</p>
<figure class="shiki javascript"><div class='codeblock'><div class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></div><div class="code"><pre class="shiki github-dark"><code><span class="line"><span style="color: #B392F0">visibleData</span><span style="color: #E1E4E8">(){</span></span>
<span class="line"><span style="color: #E1E4E8">  </span><span style="color: #F97583">let</span><span style="color: #E1E4E8"> start </span><span style="color: #F97583">=</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.start </span><span style="color: #F97583">-</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.aboveCount;</span></span>
<span class="line"><span style="color: #E1E4E8">  </span><span style="color: #F97583">let</span><span style="color: #E1E4E8"> end </span><span style="color: #F97583">=</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.end </span><span style="color: #F97583">+</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">.belowCount;</span></span>
<span class="line"><span style="color: #E1E4E8">  </span><span style="color: #F97583">return</span><span style="color: #E1E4E8"> </span><span style="color: #79B8FF">this</span><span style="color: #E1E4E8">._listData.</span><span style="color: #B392F0">slice</span><span style="color: #E1E4E8">(start, end);</span></span>
<span class="line"><span style="color: #E1E4E8">}</span></span></code></pre></div></div></figure>

<p><a target="_blank" rel="noopener" href="https://codesandbox.io/s/virtuallist-3-i3h9v">点击查看在线DEMO及完整代码</a></p>
<p>最终效果如下：</p>
<p><img onerror="imgOnError(this);" data-fancybox="gallery" src="https://cdn.tangyuxian.com/PicGoImages/20210416162829.gif" alt="img" data-caption="img" loading="lazy"></p>
<blockquote>
<p>基于这个方案，个人开发了一个基于Vue2.x的虚拟列表组件：<a target="_blank" rel="noopener" href="https://github.com/chenqf/vue-virtual-listview">vue-virtual-listview</a>,可<a target="_blank" rel="noopener" href="https://github.com/chenqf/vue-virtual-listview">点击查看完整代码</a>。</p>
</blockquote>
<h2 id="面向未来"><a href="#面向未来" class="headerlink" title="面向未来"></a>面向未来</h2><p>在前文中我们使用<code>监听scroll事件</code>的方式来触发可视区域中数据的更新，当滚动发生后，scroll事件会频繁触发，很多时候会造成<code>重复计算</code>的问题，从性能上来说无疑存在浪费的情况。</p>
<p>可以使用<a target="_blank" rel="noopener" href="https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver">IntersectionObserver</a>替换监听scroll事件，<code>IntersectionObserver</code>可以监听目标元素是否出现在可视区域内，在监听的回调事件中执行可视区域数据的更新，并且<code>IntersectionObserver</code>的监听回调是异步触发，不随着目标元素的滚动而触发，性能消耗极低。</p>
<h2 id="遗留问题"><a href="#遗留问题" class="headerlink" title="遗留问题"></a>遗留问题</h2><p>我们虽然实现了根据列表项动态高度下的虚拟列表，但如果列表项中包含图片，并且列表高度由图片撑开，由于图片会发送网络请求，此时无法保证我们在获取列表项真实高度时图片是否已经加载完成，从而造成计算不准确的情况。</p>
<p>这种情况下，如果我们能监听列表项的大小变化就能获取其真正的高度了。我们可以使用<a target="_blank" rel="noopener" href="https://developer.mozilla.org/zh-CN/docs/Web/API/ResizeObserver">ResizeObserver</a>来监听列表项内容区域的高度改变，从而实时获取每一列表项的高度。</p>
<p>不过遗憾的是，在撰写本文的时候，仅有少数<a target="_blank" rel="noopener" href="https://www.caniuse.com/#search=ResizeObserver">浏览器支持</a><code>ResizeObserver</code>。</p>

    
  </article>

  
      
    <div class="nexmoe-post-copyright">
        <strong>本文作者：</strong>云中桥<br>
        
            <strong>文章来源：</strong><a href="https://juejin.cn/post/6844903982742110216" title="https:&#x2F;&#x2F;juejin.cn&#x2F;post&#x2F;6844903982742110216" target="_blank" rel="noopener">https:&#x2F;&#x2F;juejin.cn&#x2F;post&#x2F;6844903982742110216</a><br>
        
        <strong>本文链接：</strong><a href="https://www.tangyuxian.com/2021/04/16/%E5%89%8D%E7%AB%AF/%E6%B5%8F%E8%A7%88%E5%99%A8/%E6%B5%8F%E8%A7%88%E5%99%A8-%E9%AB%98%E6%80%A7%E8%83%BD%E6%B8%B2%E6%9F%93%E5%8D%81%E4%B8%87%E6%9D%A1%E6%95%B0%E6%8D%AE-%E8%99%9A%E6%8B%9F%E5%88%97%E8%A1%A8/" title="https:&#x2F;&#x2F;www.tangyuxian.com&#x2F;2021&#x2F;04&#x2F;16&#x2F;%E5%89%8D%E7%AB%AF&#x2F;%E6%B5%8F%E8%A7%88%E5%99%A8&#x2F;%E6%B5%8F%E8%A7%88%E5%99%A8-%E9%AB%98%E6%80%A7%E8%83%BD%E6%B8%B2%E6%9F%93%E5%8D%81%E4%B8%87%E6%9D%A1%E6%95%B0%E6%8D%AE-%E8%99%9A%E6%8B%9F%E5%88%97%E8%A1%A8&#x2F;" target="_blank" rel="noopener">https:&#x2F;&#x2F;www.tangyuxian.com&#x2F;2021&#x2F;04&#x2F;16&#x2F;%E5%89%8D%E7%AB%AF&#x2F;%E6%B5%8F%E8%A7%88%E5%99%A8&#x2F;%E6%B5%8F%E8%A7%88%E5%99%A8-%E9%AB%98%E6%80%A7%E8%83%BD%E6%B8%B2%E6%9F%93%E5%8D%81%E4%B8%87%E6%9D%A1%E6%95%B0%E6%8D%AE-%E8%99%9A%E6%8B%9F%E5%88%97%E8%A1%A8&#x2F;</a><br>

        
            <strong>版权声明：</strong>本文采用 <a href="https://creativecommons.org/licenses/by-nc-sa/3.0/cn/deed.zh" target="_blank">CC BY-NC-SA 3.0 CN</a> 协议进行许可

        
    </div>


  
  
  <div class="nexmoe-post-meta nexmoe-rainbow">
   
    
        <a class="nexmoefont icon-tag-fill -none-link" href="/tags/%E6%B5%8F%E8%A7%88%E5%99%A8/" rel="tag">浏览器</a> <a class="nexmoefont icon-tag-fill -none-link" href="/tags/%E8%BD%AC%E8%BD%BD/" rel="tag">转载</a>
    
</div>
  
  
    <script async src="/js/copy-codeblock.js?v=1763992123962"></script>
  

  
      <div class="nexmoe-post-footer">
          <div class="valine"></div>
<script src='https://lib.baomitu.com/valine/1.3.9/Valine.min.js'></script>
<script>
  new Valine({
      el: '.valine',
      appId: 'r5zxC0st0DDjPA9auXzMV7HY-gzGzoHsz',
      appKey: '3bqCsovpyfTPHUzTHovd3V3V'
  })
</script>

      </div>
  
</div></div><div class="nexmoe-post-right">    <div class="nexmoe-fixed">
        <div class="nexmoe-tool">

            

            
            
            <button class="mdui-fab catalog" style="overflow:unset;">
                <i class="nexmoefont icon-i-catalog"></i>
                <div class="nexmoe-toc">
                    <ol class="toc"><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%89%8D%E8%A8%80"><span class="toc-number">1.</span> <span class="toc-text">前言</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81%E4%BD%BF%E7%94%A8%E8%99%9A%E6%8B%9F%E5%88%97%E8%A1%A8"><span class="toc-number">2.</span> <span class="toc-text">为什么需要使用虚拟列表</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E4%BB%80%E4%B9%88%E6%98%AF%E8%99%9A%E6%8B%9F%E5%88%97%E8%A1%A8"><span class="toc-number">3.</span> <span class="toc-text">什么是虚拟列表</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%AE%9E%E7%8E%B0"><span class="toc-number">4.</span> <span class="toc-text">实现</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%88%97%E8%A1%A8%E9%A1%B9%E5%8A%A8%E6%80%81%E9%AB%98%E5%BA%A6"><span class="toc-number">5.</span> <span class="toc-text">列表项动态高度</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E9%9D%A2%E5%90%91%E6%9C%AA%E6%9D%A5"><span class="toc-number">6.</span> <span class="toc-text">面向未来</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E9%81%97%E7%95%99%E9%97%AE%E9%A2%98"><span class="toc-number">7.</span> <span class="toc-text">遗留问题</span></a></li></ol>
                </div>
            </button>
            

            

            <a href="#nexmoe-content" class="backtop toc-link" aria-label="Back To Top" title="top"><button class="mdui-fab mdui-ripple"><i class="nexmoefont icon-caret-top"></i></button></a>
        </div>
    </div>
</div></div><div id="nexmoe-pendant"><div class="nexmoe-drawer mdui-drawer nexmoe-pd" id="drawer">
    
        
            <div class="nexmoe-pd-item">
                <div class="clock">
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="memory"></div>
        <div class="needle" id="hours"></div>
        <div class="needle" id="minutes"></div>
        <div class="needle" id="seconds"></div>
        <div class="clock_logo">

        </div>

    </div>
<script>
        // ----- Variable declaration

        var data = {
                dt: new Date(),
                hours: {
                        el: document.querySelector('#hours'),
                        val: 0 },

                minutes: {
                        el: document.querySelector('#minutes'),
                        val: 0 },

                seconds: {
                        el: document.querySelector('#seconds'),
                        val: 0 },

                renderTime: 1000 };


        // ----- Clock rendering logic

        updateTime = () => {
                let hour = data.dt.getHours();
                let minute = data.dt.getMinutes();
                let second = data.dt.getSeconds();

                data.hours.el.classList.add('moving');
                data.minutes.el.classList.add('moving');
                data.seconds.el.classList.add('moving');

                data.hours.val = hour == 0 && data.hours.val == 59 * 6 * 5 ? 360 : hour * 6 * 5;
                data.minutes.val = minute == 0 && data.minutes.val == 59 * 6 ? 360 : minute * 6;
                data.seconds.val = second == 0 && data.seconds.val == 59 * 6 ? 360 : second * 6;

                data.hours.el.style.transform = `translate(-50%, -75%) rotate(${data.hours.val}deg)`;
                data.minutes.el.style.transform = `translate(-50%, -75%) rotate(${data.minutes.val}deg)`;
                data.seconds.el.style.transform = `translate(-50%, -75%) rotate(${data.seconds.val}deg)`;

                data.dt.setSeconds(second + 1);
        };

        // ----- Clock execution

        setInterval(() => {
                updateTime();
        }, data.renderTime);

        updateTime();

        // ----- Events area

        data.seconds.el.addEventListener('transitionend', () => {
                data.hours.el.classList.remove('moving');
                data.minutes.el.classList.remove('moving');
                data.seconds.el.classList.remove('moving');

                // if (data.hours.val == 360) {
                //   data.hours.el.style.transform = 'translate(-50%, -75%) rotate(0deg)';
                // }

                if (data.minutes.val == 360) {
                        data.minutes.el.style.transform = 'translate(-50%, -75%) rotate(0deg)';
                }

                if (data.seconds.val == 360) {
                        data.seconds.el.style.transform = 'translate(-50%, -75%) rotate(0deg)';
                }
        });

</script>
<style>
    .clock {
        background-color: #ffffff;
        width: 70vw;
        height: 70vw;
        max-width: 70vh;
        max-height: 70vh;
        border: solid 2.8vw var(--color-primary);
        position: relative;
        overflow: hidden;
        border-radius: 50%;
        box-sizing: border-box;
        box-shadow: 0 1.4vw 2.8vw var(--color2);
        zoom:0.2
    }

    .memory {
        position: absolute;
        top: 50%;
        left: 50%;
        transform-origin: center;
    }

    .memory:nth-child(1) {
        background-color: var(--color-primary);
        width: 2%;
        height: 8%;
        transform: translate(-50%, -50%) rotate(0deg) translateY(-520%);
    }

    .memory:nth-child(2) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(6deg) translateY(-1461%);
    }

    .memory:nth-child(3) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(12deg) translateY(-1461%);
    }

    .memory:nth-child(4) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(18deg) translateY(-1461%);
    }

    .memory:nth-child(5) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(24deg) translateY(-1461%);
    }

    .memory:nth-child(6) {
        background-color: var(--color-primary);
        width: 2%;
        height: 8%;
        transform: translate(-50%, -50%) rotate(30deg) translateY(-520%);
    }

    .memory:nth-child(7) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(36deg) translateY(-1461%);
    }

    .memory:nth-child(8) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(42deg) translateY(-1461%);
    }

    .memory:nth-child(9) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(48deg) translateY(-1461%);
    }

    .memory:nth-child(10) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(54deg) translateY(-1461%);
    }

    .memory:nth-child(11) {
        background-color: var(--color-primary);
        width: 2%;
        height: 8%;
        transform: translate(-50%, -50%) rotate(60deg) translateY(-520%);
    }

    .memory:nth-child(12) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(66deg) translateY(-1461%);
    }

    .memory:nth-child(13) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(72deg) translateY(-1461%);
    }

    .memory:nth-child(14) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(78deg) translateY(-1461%);
    }

    .memory:nth-child(15) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(84deg) translateY(-1461%);
    }

    .memory:nth-child(16) {
        background-color: var(--color-primary);
        width: 2%;
        height: 8%;
        transform: translate(-50%, -50%) rotate(90deg) translateY(-520%);
    }

    .memory:nth-child(17) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(96deg) translateY(-1461%);
    }

    .memory:nth-child(18) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(102deg) translateY(-1461%);
    }

    .memory:nth-child(19) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(108deg) translateY(-1461%);
    }

    .memory:nth-child(20) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(114deg) translateY(-1461%);
    }

    .memory:nth-child(21) {
        background-color: var(--color-primary);
        width: 2%;
        height: 8%;
        transform: translate(-50%, -50%) rotate(120deg) translateY(-520%);
    }

    .memory:nth-child(22) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(126deg) translateY(-1461%);
    }

    .memory:nth-child(23) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(132deg) translateY(-1461%);
    }

    .memory:nth-child(24) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(138deg) translateY(-1461%);
    }

    .memory:nth-child(25) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(144deg) translateY(-1461%);
    }

    .memory:nth-child(26) {
        background-color: var(--color-primary);
        width: 2%;
        height: 8%;
        transform: translate(-50%, -50%) rotate(150deg) translateY(-520%);
    }

    .memory:nth-child(27) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(156deg) translateY(-1461%);
    }

    .memory:nth-child(28) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(162deg) translateY(-1461%);
    }

    .memory:nth-child(29) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(168deg) translateY(-1461%);
    }

    .memory:nth-child(30) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(174deg) translateY(-1461%);
    }

    .memory:nth-child(31) {
        background-color: var(--color-primary);
        width: 2%;
        height: 8%;
        transform: translate(-50%, -50%) rotate(180deg) translateY(-520%);
    }

    .memory:nth-child(32) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(186deg) translateY(-1461%);
    }

    .memory:nth-child(33) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(192deg) translateY(-1461%);
    }

    .memory:nth-child(34) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(198deg) translateY(-1461%);
    }

    .memory:nth-child(35) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(204deg) translateY(-1461%);
    }

    .memory:nth-child(36) {
        background-color: var(--color-primary);
        width: 2%;
        height: 8%;
        transform: translate(-50%, -50%) rotate(210deg) translateY(-520%);
    }

    .memory:nth-child(37) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(216deg) translateY(-1461%);
    }

    .memory:nth-child(38) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(222deg) translateY(-1461%);
    }

    .memory:nth-child(39) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(228deg) translateY(-1461%);
    }

    .memory:nth-child(40) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(234deg) translateY(-1461%);
    }

    .memory:nth-child(41) {
        background-color: var(--color-primary);
        width: 2%;
        height: 8%;
        transform: translate(-50%, -50%) rotate(240deg) translateY(-520%);
    }

    .memory:nth-child(42) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(246deg) translateY(-1461%);
    }

    .memory:nth-child(43) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(252deg) translateY(-1461%);
    }

    .memory:nth-child(44) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(258deg) translateY(-1461%);
    }

    .memory:nth-child(45) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(264deg) translateY(-1461%);
    }

    .memory:nth-child(46) {
        background-color: var(--color-primary);
        width: 2%;
        height: 8%;
        transform: translate(-50%, -50%) rotate(270deg) translateY(-520%);
    }

    .memory:nth-child(47) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(276deg) translateY(-1461%);
    }

    .memory:nth-child(48) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(282deg) translateY(-1461%);
    }

    .memory:nth-child(49) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(288deg) translateY(-1461%);
    }

    .memory:nth-child(50) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(294deg) translateY(-1461%);
    }

    .memory:nth-child(51) {
        background-color: var(--color-primary);
        width: 2%;
        height: 8%;
        transform: translate(-50%, -50%) rotate(300deg) translateY(-520%);
    }

    .memory:nth-child(52) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(306deg) translateY(-1461%);
    }

    .memory:nth-child(53) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(312deg) translateY(-1461%);
    }

    .memory:nth-child(54) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(318deg) translateY(-1461%);
    }

    .memory:nth-child(55) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(324deg) translateY(-1461%);
    }

    .memory:nth-child(56) {
        background-color: var(--color-primary);
        width: 2%;
        height: 8%;
        transform: translate(-50%, -50%) rotate(330deg) translateY(-520%);
    }

    .memory:nth-child(57) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(336deg) translateY(-1461%);
    }

    .memory:nth-child(58) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(342deg) translateY(-1461%);
    }

    .memory:nth-child(59) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(348deg) translateY(-1461%);
    }

    .memory:nth-child(60) {
        background-color: #949494;
        width: 1%;
        height: 3%;
        transform: translate(-50%, -50%) rotate(354deg) translateY(-1461%);
    }

    .needle {
        position: absolute;
        top: 50%;
        left: 50%;
        transform-origin: center;
    }

    .needle#hours {
        background-color: #1f1f1f;
        width: 4%;
        height: 30%;
        transform-origin: center 75%;
        transform: translate(-50%, -75%);
    }

    .needle#hours.moving {
        transition: transform 150ms ease-out;
    }

    .needle#hours:after {
        content: '';
        background-color: #1f1f1f;
        width: 4vw;
        height: 4vw;
        max-width: 4vh;
        max-height: 4vh;
        display: block;
        position: absolute;
        top: 75%;
        left: 50%;
        box-sizing: border-box;
        border-radius: 50%;
        transform: translate(-50%, -50%);
    }

    .needle#minutes {
        background-color: #1f1f1f;
        width: 2%;
        height: 45%;
        transform-origin: center 75%;
        transform: translate(-50%, -75%);
    }

    .needle#minutes.moving {
        transition: transform 150ms ease-out;
    }

    .needle#minutes:after {
        content: '';
        background-color: #1f1f1f;
        width: 4vw;
        height: 4vw;
        max-width: 4vh;
        max-height: 4vh;
        display: block;
        position: absolute;
        top: 75%;
        left: 50%;
        box-sizing: border-box;
        border-radius: 50%;
        transform: translate(-50%, -50%);
    }

    .needle#seconds {
        background-color: #cb2f2f;
        width: 1%;
        height: 50%;
        transform-origin: center 75%;
        transform: translate(-50%, -75%);
    }

    .needle#seconds.moving {
        transition: transform 150ms ease-out;
    }

    .needle#seconds:after {
        content: '';
        background-color: #cb2f2f;
        width: 2.5vw;
        height: 2.5vw;
        max-width: 2.5vh;
        max-height: 2.5vh;
        display: block;
        position: absolute;
        top: 75%;
        left: 50%;
        box-sizing: border-box;
        border-radius: 50%;
        transform: translate(-50%, -50%);
    }
    .clock_logo{
        width: 10vw;
        height: 10vw;
        max-width: 10vh;
        max-height: 10vh;
        position: absolute;
        top: 50%;
        left: 50%;
        box-sizing: border-box;
        border-radius: 50%;
        transform: translate(-50%, -50%);
        background-size: 100% 100%;
        background-repeat: no-repeat;
    }
    .clock_logo{
            width: 10vw;
            height: 10vw;
            max-width: 10vh;
            max-height: 10vh;
            position: absolute;
            top: 50%;
            left: 50%;
            box-sizing: border-box;
            border-radius: 50%;
            transform: translate(-50%, -50%);
            background-image: var(--clock-logo);
            background-size: 100% 100%;
            background-repeat: no-repeat;
    }
    @media (min-width: 100vh) {
        .clock {
            border: solid 2.8vh var(--color-primary);
            box-shadow: 0 1.4vh 2.8vh var(--color2);
        }
    }

</style>





            </div>
        
            <div class="nexmoe-pd-item">
                <div class="qweather" >
    <div id="he-plugin-standard">
        <iframe allowtransparency="true" frameborder="0" width="304" height="96" scrolling="no" src="//tianqi.2345.com/plugin/widget/index.htm?s=2&z=1&t=0&v=0&d=2&bd=0&k=000000&f=ff8040&ltf=009944&htf=cc0000&q=1&e=1&a=1&c=54511&w=300&h=96&align=center"></iframe>
    </div>
    <div class="qweather-logo">

    </div>
</div>
<style>
    .qweather{
        position: relative;
        padding-right: 5px;
    }
    .qweather #he-plugin-standard {
        border: 2px solid var(--color-primary);
        box-shadow: 0 1px 0 0 var(--color-primary), 0 1px 2px 0 var(--color-primary);
        border-radius: 8px;
        width: 300px;
        overflow: hidden;
    }
    .qweather-logo{
        position: absolute;
        right: 0;
        top: -15px;
        width: 40px;
        height: 40px;
        background-image: var(--qweather-logo);
        background-size: 100% 100%;
        background-repeat: no-repeat;
    }
</style>

            </div>
        
    
</div>
<style>
    .nexmoe-pd {
        left: auto;
        top: 40px;
        right: 0;
    }
    .nexmoe-pd-item{
       display: flex;
        justify-content: center;
        margin-bottom: 30px;
    }
    #nexmoe-pendant .mdui-drawer{
        width: 305px;
    }
    @media screen and (max-width: 1600px) {
        #nexmoe-pendant {
            display: none
        }
    }
</style>
</div><div id="nexmoe-footer"><!--!--></div><div id="nexmoe-search-space"><div class="search-container"><div class="search-header"><div class="search-input-container"><input class="search-input" type="text" placeholder="搜索" onInput="sinput();"></div><a class="search-close" onclick="sclose();">×</a></div><div class="search-body"></div></div></div><div></div><!-- hexo injector body_end start -->
<script src="https://cdn.jsdelivr.net/npm/hexo-shiki-plugin@latest/lib/codeblock.js"></script>

  <script>
  const CODE_CONFIG = {
    beautify: true,
    highlightCopy: true,
    highlightLang: true,
    highlightHeightLimit: null,
    isHighlightShrink: false,
    copy: {
      success: 'Copy Success',
      error: 'Copy Error',
      noSupport: 'Browser Not Support',
    }
  };
  console.log(
    `%c hexo-shiki-plugin %c v1.0.27 %c https://github.com/nova1751/hexo-shiki-plugin`,
    "color: #fff; background: #5f5f5f",
    "color: #fff; background: #80c8f8",
    ""
  );
  </script>
  <!-- hexo injector body_end end --><script src="/live2dw/lib/L2Dwidget.min.js?094cbace49a39548bed64abff5988b05"></script><script>L2Dwidget.init({"model":{"jsonPath":"/live2dw/assets/xiaomai.model.json"},"display":{"position":"left","width":200,"height":500},"mobile":{"show":false},"react":{"opacity":0.9},"log":false,"pluginJsPath":"lib/","pluginModelPath":"assets/","pluginRootPath":"live2dw/","tagMode":false});</script></body></html>